/*
 * EigenfaceTrainingSet.hpp
 *
 *  Created on: 31.05.2014
 *      Author: philipp
 */

#include "EigenfaceTrainingSet.hpp"

EigenfaceTrainingSet::EigenfaceTrainingSet(
		const std::vector<cv::Mat> &trainingImages, int imageSizeX,
		int imageSizeY) :
		imageSizeX(imageSizeX), imageSizeY(imageSizeY) {

	setUnknownFaceThreshold(1500);
	setNoFaceThreshold(1900);

	std::vector<cv::Mat> imageVectors;
	for (const auto &image : trainingImages) {
		auto eigenfaceVector = imageToEigenfaceVector(image);
		imageVectors.push_back(eigenfaceVector);
		this->trainingImages.push_back(eigenfaceVectorToImage(eigenfaceVector));
	}

	auto mean = calculateMeanImage(imageVectors);
	averageImage = mean.clone();

	auto trainingSet = buildEigenfacesTrainingSet(imageVectors);

	substractMeanFromTrainingSet(trainingSet, mean);

	//fast computation of eigenvectors and -values

	//transpose the training set
	cv::Mat trainingSetTransposed = trainingSet.t();

	//multiply it with the original training set
	cv::Mat trainingSetTransposedMultipliedWithTrainingSet =
			trainingSetTransposed * trainingSet;

	//calculate the eigenvalues and -vectors
	cv::Mat eigenvalues, eigenvectors;
	cv::eigen(trainingSetTransposedMultipliedWithTrainingSet, eigenvalues,
			eigenvectors);

	auto eigenvectorsAsCols = misc::exchangeRowsWithColumns(eigenvectors);

	//just take the best 30% of all eigenvectors (M')
	for (int c = 0; c < (int) ((float) eigenvectorsAsCols.cols * 1); ++c) {
		//std::cout << eigenvectorsAsCols.col(c) << std::endl;

		cv::Mat eigenvectorTimesTrainingSet = trainingSet
				* eigenvectorsAsCols.col(c);

		cv::Mat eigenvectorTimesTrainingSetNorm = normalizeVector(
				eigenvectorTimesTrainingSet);

		eigenfaces.push_back(
				misc::exchangeRowsWithColumns(eigenvectorTimesTrainingSetNorm).clone());

	}

	//calculate weights
	for (const auto &f : getTrainingImages()) {
		faceWeights.push_back(calculateFaceWeights(f));
	}

	//calculate average weight of all eigenfaces
	assert(faceWeights.size() > 0);
	cv::Mat avgWeight(faceWeights[0].rows, faceWeights[0].cols,
			faceWeights[0].type(), cv::Scalar(0));
	for (const cv::Mat &w : faceWeights) {
		avgWeight += w;
	}

	avgWeight /= faceWeights.size();
	averageFaceWeight = avgWeight.clone();

}

EigenfaceTrainingSet::~EigenfaceTrainingSet() {
}

const cv::Mat &EigenfaceTrainingSet::getAverageImage() const {
	return averageImage;
}

int EigenfaceTrainingSet::getEigenfaceCount() const {
	return eigenfaces.rows;
}

cv::Mat EigenfaceTrainingSet::getEigenface(int id) const {
	/*the eigenfaced are stored within a matrix
	 * - therefore we are returning a matrix row
	 **/
	return eigenfaces.row(id);
}

void EigenfaceTrainingSet::createDebugImages() const {
	int a = 0;

	for (const auto &i : getTrainingImages()) {
		FileHelper::saveScreenshot(i,
				"Original_Image_" + std::to_string(++a) + ".jpg", false);
	}

	FileHelper::saveScreenshot(eigenfaceVectorToImage(getAverageImage()),
			"Average_Image.jpg", true);

	for (int i = 0; i < getEigenfaceCount(); ++i) {
		FileHelper::saveScreenshot(eigenfaceVectorToImage(getEigenface(i)),
				"Eigenface_" + std::to_string(i) + ".jpg", true);
	}
}

const std::vector<cv::Mat> &EigenfaceTrainingSet::getTrainingImages() const {
	return trainingImages;
}

cv::Mat EigenfaceTrainingSet::calculateFaceWeights(const cv::Mat &image) const {

	//convert the input image to a vector
	cv::Mat imageAsVector = imageToEigenfaceVector(image);

	//substract the mean image
	cv::Mat imageMinusMean = imageAsVector - averageImage;

	//construct a vector consisting of weights
	cv::Mat weights;
	for (int i = 0; i < getEigenfaceCount(); ++i) {
		// multiply current eigenface with the diff image to get
		// the eigenface weight
		weights.push_back(getEigenface(i).dot(imageMinusMean));
	}

	return weights;
}

EigenfaceTrainingSet::FaceClassification EigenfaceTrainingSet::categorizeFace(
		const cv::Mat &image) const {

	//calculate weight of input image
	cv::Mat newImageWeights = calculateFaceWeights(image);

	//compare to known image weights
	double minEuclidianDistance = 99999999;

	//compute the euclidian distance to all know face weights
	for (const auto &w : faceWeights) {

		double euclidianDist = cv::norm(newImageWeights, w);

		if (euclidianDist < minEuclidianDistance) {
			minEuclidianDistance = euclidianDist;
		}

	}

	using namespace std;

	cout << "min dist: " << minEuclidianDistance << endl;

	if (minEuclidianDistance > noFaceThreshold) {
		//the specified image does not show a face
		return FC_NOFACE;
	}

	if (minEuclidianDistance > unknownFaceThreshold) {
		//the specified image is an unknown face
		return FC_UNKNOWN;
	}

	//the image is known!
	return FC_KNOWN;

}

void EigenfaceTrainingSet::setUnknownFaceThreshold(double threshold) {
	unknownFaceThreshold = threshold;
}

void EigenfaceTrainingSet::setNoFaceThreshold(double threshold) {
	noFaceThreshold = threshold;
}

cv::Mat EigenfaceTrainingSet::normalizeVector(const cv::Mat &vector) const {
	double length = 0;

	for (int c = 0; c < vector.cols; ++c) {
		for (int r = 0; r < vector.rows; ++r) {
			length += pow(vector.at<double>(cv::Point(c, r)), 2);
		}
	}

	length = sqrt(length);

	return vector / length;
}

cv::Mat EigenfaceTrainingSet::eigenfaceVectorToImage(
		const cv::Mat &vector) const {
	cv::Mat image(vector.cols / imageSizeX, imageSizeX, vector.type());

	for (int r = 0; r < image.rows; ++r) {
		for (int c = 0; c < image.cols; ++c) {
			image.at<double>(cv::Point(c, r)) = vector.at<double>(
					cv::Point(r * image.cols + c, 0));
		}
	}

	return image;
}

cv::Mat EigenfaceTrainingSet::imageToEigenfaceVector(
		const cv::Mat &screenshot) const {

	//resize the input image to fit the default eigenface size
	cv::Mat scResized;
	cv::resize(screenshot, scResized, cv::Size(imageSizeX, imageSizeY), 0, 0,
			CV_INTER_LINEAR);

	//allocate 1 dimensional matrix as image vector
	cv::Mat imageAsVector(1, scResized.rows * scResized.cols, scResized.type());

	//copy matrix into vector
	for (int r = 0; r < scResized.rows; ++r) {
		for (int c = 0; c < scResized.cols; ++c) {
			imageAsVector.at<double>(cv::Point(r * scResized.cols + c, 0)) =
					scResized.at<double>(cv::Point(c, r));
		}
	}

	return imageAsVector;
}

cv::Mat EigenfaceTrainingSet::buildEigenfacesTrainingSet(
		const std::vector<cv::Mat> &columns) const {
	//we need to have at least one image in the traning set
	assert(columns.size() > 0);

	//combine all eigenface vectors to one big matrix
	cv::Mat trainingSet;
	for (size_t c = 0; c < columns.size(); ++c) {
		trainingSet.push_back(columns[c]);
	}

	return misc::exchangeRowsWithColumns(trainingSet);
}

cv::Mat EigenfaceTrainingSet::calculateMeanImage(
		const std::vector<cv::Mat> &images) const {
	//we require at least one image
	assert(images.size() > 0);

	//new matrix initialized with zeroes to store average image
	cv::Mat mean(images[0].rows, images[0].cols, CV_64F, cv::Scalar(0));
	for (const auto &image : images) {
		mean += image;
	}

	mean /= cv::Mat(mean.rows, mean.cols, mean.type(),
			cv::Scalar(images.size()));

	return mean;
}

void EigenfaceTrainingSet::substractMeanFromTrainingSet(cv::Mat &trainingSet,
		const cv::Mat &meanImage) const {

	const cv::Mat meanImageAsColumns = misc::exchangeRowsWithColumns(meanImage);

	//substract mean from every single column (image) of the training set
	for (int x = 0; x < trainingSet.cols; ++x) {
		trainingSet.col(x) -= meanImageAsColumns;
	}
}

